package com.letoken.platform.pub.util;

import com.alibaba.fastjson.JSONObject;
import com.letoken.platform.pub.enums.LetokenApiResponseEnum;
import com.letoken.platform.pub.exception.ApiServiceException;
import com.letoken.platform.pub.res.web3.BscBalance;
import com.letoken.platform.pub.res.web3.BscResponse;
import com.letoken.platform.pub.res.web3.BscTransaction;
import com.letoken.platform.pub.res.token.TokenErc20TxRes;
import com.letoken.platform.pub.res.token.TokenTxRes;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpHost;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.web3j.abi.FunctionEncoder;
import org.web3j.abi.TypeReference;
import org.web3j.abi.datatypes.Address;
import org.web3j.abi.datatypes.Function;
import org.web3j.abi.datatypes.Type;
import org.web3j.abi.datatypes.generated.Uint256;
import org.web3j.crypto.*;
import org.web3j.protocol.Web3j;
import org.web3j.protocol.core.methods.response.EthSendTransaction;
import org.web3j.protocol.core.methods.response.TransactionReceipt;
import org.web3j.protocol.http.HttpService;
import org.web3j.tx.Transfer;
import org.web3j.utils.Convert;
import org.web3j.utils.Numeric;

import javax.annotation.PostConstruct;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.*;

import static com.letoken.platform.pub.util.EthUtil.*;


/**
 * @author letokenlabs
 * @date 2022/11/17$
 */
@Slf4j
@Component
public class BscUtil {

    public static String url;
    public static String testnetUrl;
    public static String apikey;

    private static String proxyHost;
    private static boolean enableProxy;

    @Value("${bscscan.api.url:https://api.bscscan.com/api}")
    private String _url;

    @Value("${bscscan.testnet.api.url:https://api-testnet.bscscan.com/api}")
    private String _testnetUrl;

    @Value("${bscscan.api.apikey:}")
    private String _apikey;

    @Value("${bscscan.api.proxyHost:http://127.0.0.1:1080}")
    private String _proxyHost;

    @Value("${bscscan.api.proxy:false}")
    private boolean _enableProxy;

    @PostConstruct
    public void init() {
        url = _url;
        testnetUrl = _testnetUrl;
        apikey = _apikey;

        proxyHost = _proxyHost;
        enableProxy = _enableProxy;
    }

    private static HttpHost getProxy() {
        return enableProxy ? HttpHost.create(proxyHost) : null;
    }

    /**
     * 获取Web3j对象（支持代理）
     * @param endpoint
     * @return
     */
    public static Web3j getWeb3j(String endpoint) {
        if (enableProxy) {
            return Web3j.build(new HttpService(endpoint, HttpClientUtil.getProxyClient(proxyHost)));
        }
        else {
            return Web3j.build(new HttpService(endpoint));
        }
    }

    /**
     * 获取网络URL
     * @param network
     * @return
     */
    private static String getApiUrl(String network) {
       return (network.equals("bsc") || network.equals("56")) ? url: testnetUrl;
    }

    /**
     *  获取最新区块高度
     * @param network 网络名称或ID(bsc/bsctestnet,56/97)
     * @return
     */
    public static long blockHeight(String network) {

        HashMap<String, String> parame = new HashMap<>();
        parame.put("module", "block");
        parame.put("action", "getblocknobytime");
        parame.put("timestamp", DateUtil.getUnixTimestamp());
        parame.put("closest", "before");
        parame.put("apikey", apikey);
        String res = null;
        try {
            res = HttpClientUtil.sendGet(getApiUrl(network), parame, null, getProxy());
        } catch (Exception e) {
            throw new ApiServiceException(LetokenApiResponseEnum.ERROR_WALLET_TX_001);
        }
        BscResponse bscResponse = JSONObject.parseObject(res, BscResponse.class);
        if (bscResponse.getStatus().equals("1")) {
            return Long.parseLong(bscResponse.getResult());
        } else {
            throw new ApiServiceException(bscResponse.getMessage());
        }
    }

    /**
     * 根据交易hash获取交易状态
     * @param network 网络名称或ID(bsc/bsctestnet,56/97)
     * @return
     */
    public static String getTxReceiptstatus(String txhash, String network) {
        Boolean flag = false;
        HashMap<String, String> parame = new HashMap<>();
        parame.put("module", "transaction");
        parame.put("action", "gettxreceiptstatus");
        parame.put("txhash", txhash);
        parame.put("apikey", apikey);
        String res = null;
        try {
            res = HttpClientUtil.sendGet(getApiUrl(network), parame, null, getProxy());
        } catch (Exception e) {
            throw new ApiServiceException(LetokenApiResponseEnum.ERROR_WALLET_TX_001);
        }
        BscResponse bscResponse = JSONObject.parseObject(res, BscResponse.class);
        if (bscResponse.getStatus().equals("1")) {
            JSONObject result = JSONObject.parseObject(bscResponse.getResult());
            return result.get("status").toString();
        } else {
            throw new ApiServiceException(bscResponse.getMessage());
        }
    }


    /**
     * 获取单个地址的BNB余额
     *
     * @param address 钱包地址
     * @param network 网络名称或ID(bsc/bsctestnet,56/97)
     * @return wei
     */
    public static String getBalance(String address, String network) {
        HashMap<String, String> parame = new HashMap<>();
        parame.put("module", "account");
        parame.put("action", "balance");
        parame.put("address", address);
        parame.put("apikey", apikey);
        String res = null;
        try {
            res = HttpClientUtil.sendGet(getApiUrl(network), parame, null, getProxy());
        } catch (Exception e) {
            throw new ApiServiceException(LetokenApiResponseEnum.ERROR_WALLET_TX_001);
        }
        BscResponse bscResponse = JSONObject.parseObject(res, BscResponse.class);
        if (bscResponse.getStatus().equals("1")) {
            return bscResponse.getResult();
        } else {
            throw new ApiServiceException(bscResponse.getMessage());
        }
    }


    /**
     * 获取多个地址的BNB余额
     * @param addressList 钱包地址(逗号分隔)
     * @param network 网络名称或ID(bsc/bsctestnet,56/97)
     * @return
     */
    public static List<BscBalance> getBalanceMulti(String addressList, String network) {
        HashMap<String, String> parame = new HashMap<>();
        parame.put("module", "account");
        parame.put("action", "balancemulti");
        parame.put("address", addressList);
        parame.put("tag", "latest");
        parame.put("apikey", apikey);
        String res = null;
        try {
            res = HttpClientUtil.sendGet(getApiUrl(network), parame, null, getProxy());
        } catch (Exception e) {
            throw new ApiServiceException(LetokenApiResponseEnum.ERROR_WALLET_TX_001);
        }
        BscResponse bscResponse = JSONObject.parseObject(res, BscResponse.class);
        List<BscBalance> bscBalances = null;
        if (bscResponse.getStatus().equals("1")) {
            bscBalances = JSONObject.parseObject(bscResponse.getResult(), List.class);
        } else {
            throw new ApiServiceException(bscResponse.getMessage());
        }
        return bscBalances;
    }

    /**
     * 获取钱包代币余额（erc20)
     * @param tokenAddress 代币合约地址
     * @param address 用户钱包地址
     * @return wei
     */
    public static String getTokenBalance(String tokenAddress, String address, String network) {
        HashMap<String, String> parame = new HashMap<>();
        parame.put("module", "account");
        parame.put("action", "tokenbalance");
        parame.put("contractaddress", tokenAddress);
        parame.put("address", address);
        parame.put("tag", "latest");
        parame.put("apikey", apikey);
        String res = null;
        try {
            res = HttpClientUtil.sendGet(getApiUrl(network), parame, null, getProxy());
        } catch (Exception e) {
            throw new ApiServiceException(LetokenApiResponseEnum.ERROR_WALLET_TX_001);
        }
        BscResponse bscResponse = JSONObject.parseObject(res, BscResponse.class);
        if (bscResponse.getStatus().equals("1")) {
            return bscResponse.getResult();
        } else {
            throw new ApiServiceException(bscResponse.getMessage());
        }
    }

    /**
     * 获取钱包Hash Internal记录
     * @param txhash txhash
     * @param network network
     * @return wei
     */
    public static List<BscTransaction> getTxListInternal(String txhash, String network) {
        Map<String, String> parame = new HashMap<>();
        parame.put("module", "account");
        parame.put("action", "txlistinternal");
        parame.put("txhash", txhash);
        parame.put("apikey", apikey);
        String res = null;
        try {
            res = HttpClientUtil.sendGet(getApiUrl(network), parame, null, getProxy());
        } catch (Exception e) {
            throw new ApiServiceException(LetokenApiResponseEnum.ERROR_WALLET_TX_001);
        }
        BscResponse bscResponse = JSONObject.parseObject(res, BscResponse.class);
        List<BscTransaction> bscTransaction = null;
        if (bscResponse.getStatus().equals("1")) {
            bscTransaction = JSONObject.parseObject(bscResponse.getResult(), List.class);
        } else {
            throw new ApiServiceException(bscResponse.getMessage());
        }
        return bscTransaction;
    }


    /**
     * 获取GasPrice
     * @param network 网络名称或ID(bsc/bsctestnet,56/97)
     * @return wei
     */
    public static BigInteger getGasPrice(String network) {
        HashMap<String, String> parame = new HashMap<>();
        parame.put("module", "gastracker");
        parame.put("action", "gasoracle");
        parame.put("apikey", apikey);
        String res = null;
        try {
            res = HttpClientUtil.sendGet(url, parame, null, getProxy());
        } catch (Exception e) {
            throw new ApiServiceException(LetokenApiResponseEnum.ERROR_WALLET_TX_001);
        }
        BscResponse bscResponse = JSONObject.parseObject(res, BscResponse.class);
        if (bscResponse.getStatus().equals("1")) {
            JSONObject result = JSONObject.parseObject(bscResponse.getResult());
            return new BigInteger(result.get("ProposeGasPrice").toString()).multiply(BigInteger.valueOf(10).pow(9));
        } else {
            throw new ApiServiceException(bscResponse.getMessage());
        }
    }

    /**
     * 发送原生代币
     * @param privateKey 钱包秘钥
     * @param toAddress 收款钱包地址
     * @param value 转账数量(wei)
     * @return txHash
     * @throws Exception
     */
    public static String transfer(Web3j web3j, String privateKey, String toAddress, BigDecimal value) throws Exception {

        if (web3j != null) {
            try {
                Credentials credentials = Credentials.create(privateKey);
                TransactionReceipt transactionReceipt = Transfer.sendFunds(web3j, credentials, toAddress, value, Convert.Unit.WEI).send();
                return transactionReceipt.getTransactionHash();
            } catch (Throwable t) {
                log.error(String.format("发送BNB失败 from:%s to:%s amount-bnb:%s",
                        getAddressByPrivateKey(privateKey), toAddress, toEth(value.toBigInteger()).toString()));
            }
        }
        return null;
    }

    public static String transferToken(Web3j web3j, String contractAddress, String privateKey, String toAddress, BigDecimal value, BigInteger gasPrice, BigInteger gasLimit) {

        if (web3j != null) {
            String from = getAddressByPrivateKey(privateKey);
            log.info(String.format("Start:SendErc20 from:%s to:%s amount:%s erc20:%s", from, toAddress, value.toString(), contractAddress));
            try {
                //加载转账所需的凭证，用私钥
                Credentials credentials = Credentials.create(privateKey);
                //获取nonce，交易笔数
                BigInteger nonce = getNonce(web3j, from);
                if (nonce == null) {
                    log.error(String.format("END:GetNonceError from:%s to:%s amount:%s erc20:%s", from, toAddress, value.toString(), contractAddress));
                    return null;
                }

                if (gasPrice.equals(0)) {
                    log.error(String.format("END:GetGasPriceError from:%s to:%s amount:%s erc20:%s", from, toAddress, value.toString(), contractAddress));
                    return null;
                }

                //ERC20代币合约方法
                Function function = new Function(
                        "transfer",
                        Arrays.asList(new Address(toAddress), new Uint256(value.toBigInteger())),
                        Collections.singletonList(new TypeReference<Type>() {
                        }));

                //创建RawTransaction交易对象
                String encodedFunction = FunctionEncoder.encode(function);
                RawTransaction rawTransaction = RawTransaction.createTransaction(nonce, gasPrice, gasLimit,
                        contractAddress, encodedFunction);


                //签名Transaction
                byte[] signMessage = TransactionEncoder.signMessage(rawTransaction, credentials);
                String hexValue = Numeric.toHexString(signMessage);
                //发送交易
                EthSendTransaction ethSendTransaction = web3j.ethSendRawTransaction(hexValue).sendAsync().get();
                String hash = ethSendTransaction.getTransactionHash();
                if (hash != null) {
                    return hash;
                }
                log.error(String.format("END:HashIsNull from:%s to:%s amount:%s erc20:%s >> %s",
                        from, toAddress, value.toString(), contractAddress, ethSendTransaction.getError().getMessage()));
            } catch (Throwable t) {
                log.error(String.format("发送ERC20失败 from=%s to=%s erc20=%s amount=%s >> %s",
                        from, toAddress, contractAddress, value.toString(), t.getMessage()), t);
            }
        }
        return null;
    }

    public static List<TokenErc20TxRes> getTokenErc20Txs(String network, String contractAddress, String walletAddress, Integer page, Integer pageSize, String sort) {
        Map<String, String> parame = new HashMap<>();
        parame.put("module", "account");
        parame.put("action", "tokentx");
        parame.put("contractaddress", contractAddress);
        parame.put("address", walletAddress);
        parame.put("page", page.toString());
        parame.put("offset", pageSize.toString());
        parame.put("sort", sort);
        parame.put("apikey", apikey);
        String res = null;
        try {
            res = HttpClientUtil.sendGet(getApiUrl(network), parame, null, getProxy());
        } catch (Exception e) {
            throw new ApiServiceException(LetokenApiResponseEnum.ERROR_WALLET_TX_001);
        }
        BscResponse bscResponse = JSONObject.parseObject(res, BscResponse.class);
        List<TokenErc20TxRes> list = null;
        if (bscResponse.getStatus().equals("1")) {
            list = JSONObject.parseArray(bscResponse.getResult(), TokenErc20TxRes.class);
        } else {
            throw new ApiServiceException(bscResponse.getMessage());
        }
        return list;
    }

    public static List<TokenTxRes> getTokenTxs(String network, String walletAddress, Integer page, Integer pageSize, String sort) {
        Map<String, String> parame = new HashMap<>();
        parame.put("module", "account");
        parame.put("action", "txlist");
        parame.put("address", walletAddress);
        parame.put("page", page.toString());
        parame.put("offset", pageSize.toString());
        parame.put("sort", sort);
        parame.put("apikey", apikey);
        String res = null;
        try {
            res = HttpClientUtil.sendGet(getApiUrl(network), parame, null, getProxy());
        } catch (Exception e) {
            throw new ApiServiceException(LetokenApiResponseEnum.ERROR_WALLET_TX_001);
        }
        BscResponse bscResponse = JSONObject.parseObject(res, BscResponse.class);
        List<TokenTxRes> list = null;
        if (bscResponse.getStatus().equals("1")) {
            list = JSONObject.parseArray(bscResponse.getResult(), TokenTxRes.class);
        } else {
            throw new ApiServiceException(bscResponse.getMessage());
        }
        return list;
    }
}
